﻿
package extremefx.i18n {
	import extremefx.DateTime;
	import extremefx.DayOfWeek;
	import extremefx.ICloneable;
	import extremefx.TimeSpan;	

	/**
	 * @author Marcelo Volmaro
	 * The class serves as a base class for calendar classes.
	 */
	public class Calendar implements ICloneable {
		/**
		 * The protected member stores the value for the twoDigitYearMax
		 */
		internal var _twoDigitYearMax:int;
		
		/**
		 * Protected field storing the abbreviated era names.
		 */
		protected var _abbrEraNames:Array;
		/**
		 * Protected field storing the era names.
		 */
		internal var _eraNames:Array;
		
		/**
		 * Get-only property returing the maximum allowed year for this class.
		 */
		protected var _maxYearValue:int = 0;
		
		public function Calendar() {
			_twoDigitYearMax = 99;
		}
		
		public function get abbreviatedEraNames():Array {
			if (_abbrEraNames == null || _abbrEraNames.length != eras.length) throw new Error("Internal: _abbrEraNames wrong initialized!");
			return _abbrEraNames.concat();
		}
		
		public function set abbreviatedEraNames(pEras:Array):void {
			if (pEras.length != eras.length) {
				throw new ArgumentError(XString.format("Array length must be equal Eras length {0}.", eras.length));
			}
			 
			_abbrEraNames = pEras.concat();
		}
		
		public function get eraNames():Array {
			if (_eraNames == null || _eraNames.length != eras.length) throw new Error("Internal: _eraNames wrong initialized!");
			return _eraNames.concat();
		}
		
		public function set eraNames(pEras:Array):void {
			if (pEras.length != eras.length) {
				throw new ArgumentError(XString.format("Array length must be equal Eras length {0}.", eras.length));
			}
			 
			_eraNames = pEras.concat();
		}
		
		/**
		 * An protected integer property that gives the number of days in a week. It might be overridden.
		 */
		public function get _daysInWeek():int{
			return 7;
		}	
		
		/**
		 * The protected method creates the string used in the RangeError
		 * @param pA An object that represents the smallest allowable value.
		 * @param pB An object that represents the greatest allowable value.
		 * @return The string used in the RangeError.
		 */
		protected function _validValues(pA:*, pB:*):String{
			return XString.format("Valid values are between {0} and {1}, inclusive.", pA, pB);
		}
		
		/**
		 * The protected method checks wether the parameter <code>pArg</code> is in the allowed range.
		 * @param pParam A string that gives the name of the parameter to check.
		 * @param pArg An integer that gives the value to check.
		 * @param pA An integer that represents the smallest allowed value.
		 * @param pB An integer that represents the greatest allowed value.
		 * @throws RangeError The exception is thrown, if the <code>pArg</code> is outside the allowed range.
		 */
		protected function _argumentInRange(pParam:String, pArg:int, pA:int, pB:int):void {
			if (pA <= pArg && pArg <= pB) return;
			throw new RangeError(pParam, _validValues(pA, pB));
		}
		
		
		protected function _checkHMSM(pHour:int, pMinute:int, pSecond:int, pMilliseconds:int):void {
			_argumentInRange("hour", pHour, 0, 23);
			_argumentInRange("minute", pMinute, 0, 59);
			_argumentInRange("second", pSecond, 0, 59);
			_argumentInRange("milliseconds", pMilliseconds, 0, 999999);
		}
		
		protected function _getMaxYear():int {
			if (_maxYearValue == 0) {
				_maxYearValue = getYear(DateTime.MAX_VALUE);
			}
			return _maxYearValue;
		}
		
		/**
		 * Checks whether the year is the era is valid, if era = CurrentEra the right value is returned.
		 * @param pYear The year to check.
		 * @param pEra The era to check.
		 * @throws RangeError The exception will be thrown, if the year is not valid.
		 */
		protected function _checkYE(pYear:int, pEra:Array):void {

		}
		
		/**
		 * A protected method to calculate the number of days between two dates.
		 * @param pDateA A <code>DateTime</code> representing the first date.
		 * @param pDateB A <code>DateTime</code> representing the second date.
		 * @return An integer that represents the difference of days between <code>pDateA</code> and <code>pDateB</code>
		 */
		
		protected function _diffDays(pDateA:DateTime, pDateB:DateTime):int {
			var diff:Number = pDateA.ticks - pDateB.ticks;

			if (diff >= 0) {
				return int(diff/TimeSpan.TICKS_PER_DAY);
			}

			diff++;
			return -1 + int(diff/TimeSpan.TICKS_PER_DAY);			
		}
		
		/**
		 * A protected method that gives the first day of the second week of the year.
		 * @param pYear An integer that represents the year.
		 * @param pRule The <code>CalendarWeekRule</code> to be used for the calculation.
		 * @param pFirstDayOfWeek The <code>DayOfWeek</code> specifying the first day in a week.
		 * @return The <code>DateTime</code> representing the first day of the second week of the year.
		 */
		protected function _getFirstDayOfSecondWeekOfYear(pYear:int, pRule:uint, pFirstDayOfWeek:uint):DateTime {
			var d1:DateTime = toDateTime(pYear, 1, 1, 0, 0, 0, 0);
			
			var dow1:int = getDayOfWeek(d1);
			var fdow:int = pFirstDayOfWeek;
			var d:int = 0;
	
			switch (pRule) {
				case CalendarWeekRule.FIRST_DAY:
					if (fdow > dow1) {
						d += fdow - dow1;
						
					} else {
						d += fdow + _daysInWeek - dow1;
					}
				break;
				
				case CalendarWeekRule.FIRST_FULL_WEEK:
					d = _daysInWeek;
					
					if (fdow >= dow1) {
						d += fdow - dow1;
						
					} else {
						d += fdow + _daysInWeek - dow1;
					}
					
				break;
				
				case CalendarWeekRule.FIRST_FOURDAY_WEEK:
					var dow4:int = (dow1 + 3) % _daysInWeek;
		
					d = 3;
					if (fdow > dow4) {
						d += fdow - dow4;
						
					} else {
						d += fdow + _daysInWeek - dow4;
					}
				}
	
			return addDays(d1, d);
		}
		
		/**
		 * When overridden gives the eras supported by the calendar as an array of integers.
		 */
		public function get eras():Array {
			return [];
		}
		
		public function get algorithmType():uint {
			return CalendarAlgorithmType.UNKNOWN;
		}
		
		public function get maxSupportedDateTime():DateTime {
			return DateTime.MAX_VALUE;
		}
		
		public function get minSupportedDateTime():DateTime {
			return DateTime.MIN_VALUE;
		}
		
		/**
		 * TODO: clone()
		 */
		public function clone():Object {
			return null;
		}
		
		public function getLeapMonth(pYear:int, pEra:int = -1):int {
			if (pEra == -1) pEra = getEra(toDateTime (pYear, 1, 1, 0, 0, 0, 0));
			var max:int = getMonthsInYear (pYear, pEra);
			
			for (var i:int = 1; i <= max; i++){
				if (isLeapMonth (pYear, i, pEra)){
					return i;
				}
			}
			
			return 0;
		}
		
		public function get twoDigitYearMax():int {
			return _twoDigitYearMax;
		}
		
		public function set twoDigitYearMax(pYear:int):void{
			_argumentInRange("year", pYear, 100, _getMaxYear());
			var era:int = -1;
			_checkYE(pYear, [era]);
			_twoDigitYearMax = pYear;
		}
		
		/**
		 * The virtual method adds days to a given date.
		 * @param pTime The <code>DateTime</code> to which to add days.
		 * @param pDays The number of days to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pDays</code> to the specified DateTime.
		 */
		public function addDays(pTime:DateTime, pDays:int):DateTime {
			var d:DateTime = new DateTime(pTime);
			d.add(TimeSpan.fromDays(pDays));
			return d;
		}
		
		/**
		 * The virtual method adds hours to a given date.
		 * @param pTime The <code>DateTime</code> to which to add hours.
		 * @param pHours The number of hours to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pHours</code> to the specified DateTime.
		 */
		public function addHours(pTime:DateTime, pHours:int):DateTime {
			var d:DateTime = new DateTime(pTime);
			d.add(TimeSpan.fromHours(pHours));
			return d;
		}
		
		/**
		 * The virtual method adds milliseconds to a given date.
		 * @param pTime The <code>DateTime</code> to which to add milliseconds.
		 * @param pMSeconds The number of milliseconds to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pMSeconds</code> to the specified DateTime.
		 */
		public function addMilliseconds(pTime:DateTime, pMSeconds:int):DateTime {
			var d:DateTime = new DateTime(pTime);
			d.add(TimeSpan.fromMilliseconds(pMSeconds));
			return d;
		}
		
		/**
		 * The virtual method adds minutes to a given date.
		 * @param pTime The <code>DateTime</code> to which to add minutes.
		 * @param pMinutes The number of minutes to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pMinutes</code> to the specified DateTime.
		 */
		public function addMinutes(pTime:DateTime, pMinutes:int):DateTime {
			var d:DateTime = new DateTime(pTime);
			d.add(TimeSpan.fromMinutes(pMinutes));
			return d;
		}
		
		/**
		 * The virtual method adds seconds to a given date.
		 * @param pTime The <code>DateTime</code> to which to add seconds.
		 * @param pSeconds The number of seconds to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pSeconds</code> to the specified DateTime.
		 */
		public function addSeconds(pTime:DateTime, pSeconds:int):DateTime {
			var d:DateTime = new DateTime(pTime);
			d.add(TimeSpan.fromSeconds(pSeconds));
			return d;
		}
		
		/**
		 * The virtual method adds weeks to a given date.
		 * @param pTime The <code>DateTime</code> to which to add weeks.
		 * @param pWeeks The number of weeks to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pWeeks</code> to the specified DateTime.
		 */
		public function addWeeks(pTime:DateTime, pWeeks:int):DateTime {
			var d:DateTime = new DateTime(pTime);
			d.addDays(pWeeks * _daysInWeek);
			return d;
		}
		
		/**
		 * When overrideden adds months to a given date.
		 * @param pTime The <code>DateTime</code> to which to add months.
		 * @param pMonths The number of months to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pMonths</code> to the specified DateTime.
		 */
		public function addMonths(pTime:DateTime, pMonths:int):DateTime {
			return null;
		}
		
		/**
		 * When overrideden adds years to a given date.
		 * @param pTime The <code>DateTime</code> to which to add years.
		 * @param pYears The number of years to add.
		 * @return A new <code>DateTime</code> value, that results from adding <code>pYears</code> to the specified DateTime.
		 */
		public function addYears(pTime:DateTime, pYears:int):DateTime {
			return null;
		}
		
		/**
		 * When overriden gets the day of the month from <code>pTime</code>
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer giving the day of months, starting with 1.
		 */
		public function getDayOfMonth(pTime:DateTime):int {
			return 0;
		}
		
		/**
		 * When overriden gets the day of the week from the specified date.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer giving the day of the week, starting at 0. (listed from <code>DayOfWeek</code>)
		 */
		public function getDayOfWeek(pTime:DateTime):int {
			return -1;
		}
		
		/**
		 * When overridden gives the number of the day in the year.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer representing the day of the year, starting with 1.
		 */
		public function getDayOfYear(pTime:DateTime):int {
			return 0;
		}
		
		/**
		 * When overridden gives the number of days in the specified month of the given year and era.
		 * @param pYear An integer that gives the year.
		 * @param pMonth An integer that gives the month, starting with 1.
		 * @param pEra An intger that gives the era of the specified year, or none for use the currentEra.
		 * @return An integer that gives the number of days of the specified month.
		 * @throws RangeError The exception is thrown, if pYear, pMonth or pEra are outside the allowed range.
		 */
		public function getDaysInMonth(pYear:int, pMonth:int, pEra:int = -1):int {
			return -1;
		}
		
		/**
		 * When overridden gives the number of days of the specified year of the given era.
		 * @param pYear An integer that gives the year.
		 * @param pEra An intger that gives the era of the specified year, or none for use the currentEra.
		 * @return An integer that gives the number of days of the specified year.
		 * @throws RangeError The exception is thrown, if pYear or pEra are outside the allowed range.
		 */
		public function getDaysInYear(pYear:int, pEra:int = -1):int {
			return -1;
		}
		
		/**
		 * When overridden gives the era of the specified date.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer representing the era of the calendar.
		 */
		public function getEra(pTime:DateTime):int {
			return -1;
		}
		
		/**
		 * Virtual method that gives the hour of the specified time.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer that gives the hour of the specified time, starting with 0.
		 */
		public function getHour(pTime:DateTime):int {
			return pTime.timeOfDay.hours;
		}
		
		/**
		 * Virtual method that gives the milliseconds of the specified time.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer that gives the milliseconds of the specified time, starting with 0.
		 */
		public function getMilliseconds(pTime:DateTime):int {
			return pTime.timeOfDay.milliseconds;
		}
		
		/**
		 * Virtual method that gives the minute of the specified time.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer that gives the minute of the specified time, starting with 0.
		 */
		public function getMinute(pTime:DateTime):int {
			return pTime.timeOfDay.minutes;
		}
		
		/**
		 * Virtual method that gives the second of the specified time.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer that gives the second of the specified time, starting with 0.
		 */
		public function getSecond(pTime:DateTime):int {
			return pTime.timeOfDay.seconds;
		}
		
		/**
		 * When overridden gives the number of the month of the specified date.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer representing the month, starting with 1.
		 */
		public function getMonth(pTime:DateTime):int {
			return -1;
		}
		
		/**
		 * Virtual method that gives the number of months in the specified year and era.
		 * @param pYear An integer that specifies the year.
		 * @param pEra An integer that specifies the era, or nothing for use the currentEra.
		 * @return An integer that gives the number of the months in the specified year.
		 * @throws RangeError The exception is thrown, if the year or the era are not valid.
		 */
		public function getMonthsInYear(pYear:int, pEra:int = -1):int {
			return -1;
		}
		
		/**
		 * A virtual method that gives the number of the week in the year.
		 * @param pTime A <code>DateTime</code> representing the date.
		 * @param pRule The <code>CalendarWeekRule</code> to be used for the calculation.
		 * @param pFirstDayOfWeek The <code>DayOfWeek</code> specifying the first day in a week.
		 * @return An integer representing the number of the week in the year, starting with 1.
		 */
		
		public function getWeekOfYear(pTime:DateTime, pRule:uint, pFirstDayOfWeek:uint):int {
			if (pFirstDayOfWeek < DayOfWeek.SUNDAY || DayOfWeek.SATURDAY < pFirstDayOfWeek) {
			    	throw new RangeError("firstDayOfWeek", "Value is not a valid day of week.");
			}
			
			var year:int = getYear(pTime);
			var days:int;
	
			while (true) {
				var secondWeek:DateTime = _getFirstDayOfSecondWeekOfYear(year, pRule, pFirstDayOfWeek);
				days = _diffDays(pTime, secondWeek) + _daysInWeek;
				if (days >= 0) break;
				year -= 1;
			}
	
			return 1 + days/_daysInWeek;
		}
		
		/**
		 * When overridden gives the number of the year of the specified date.
		 * @param pTime The <code>DateTime</code> that specifies a date.
		 * @return An integer representing the year, starting with 1.
		 */
		public function getYear(pTime:DateTime):int {
			return -1;
		}
		
		/**
		 * Tells when overridden whether the given day is a leap day.
		 * @param pYear An integer that specifies the year in the given era.
		 * @param pMonth An integer that specifies the month.
		 * @param pDay An integer that specifies the day.
		 * @param pEra An integer that specifies the era, or nothing for currentEra.
		 * @return A boolean that tells whether the given day is a leap day.
		 */
		public function isLeapDay(pYear:int, pMonth:int, pDay:int, pEra:int = -1):Boolean {
			return false;
		}
		
		/**
		 * Tells when overridden whether the given month is a leap month.
		 * @param pYear An integer that specifies the year in the given era.
		 * @param pMonth An integer that specifies the month.
		 * @param pEra An integer that specifies the era, or nothing for currentEra.
		 * @return A boolean that tells whether the given day is a leap day.
		 */
		public function isLeapMonth(pYear:int, pMonth:int, pEra:int = -1):Boolean {
			return false;
		}
		
		/**
		 * Tells when overridden whether the given month is a leap month.
		 * @param pYear An integer that specifies the year in the given era.
		 * @param pEra An integer that specifies the era, or nothing for currentEra.
		 * @return A boolean that tells whether the given day is a leap day.
		 */
		public function isLeapYear(pYear:int, pEra:int = -1):Boolean {
			return false;
		}
		
		/**
		 * When overridden creates the <code>DateTime</code> from the parameters.
		 * @param pYear An integer that gives the year in the era
		 * @param pMonth An integer that specifies the month.
		 * @param pDay An integer that specifies the day.
		 * @param pHour An integer that specifies the hour.
		 * @param pMinute An integer that specifies the minute.
		 * @param pSecond An integer that gives the second.
		 * @param pMSecond An integer that gives the milliseconds.
		 * @param pEra An integer that specifies the era, or nothing for currentEra.
		 * @return A <code>DateTime</code> representig the date and time.
		 * @throws RangeError The exception is thrown, if at least one of the parameters is out of range.
		 */
		public function toDateTime(pYear:int, pMonth:int, pDay:int, pHour:int, pMinute:int, pSecond:int, pMSecond:int, pEra:int = -1):DateTime {
			return null;
		}
		
		/**
		 * A virtual method that converts a two-digit year to a four-digit year. It uses the <code>twoDigitYearMax</code> property.
		 * @param pYear An integer that gives the two-digit year.
		 * @return An integer giving the four digit year.
		 * @throws RangeError The exception is thrown if the year is negative or the resulting year is invalid.
		 */
		public function toFourDigitYear(pYear:int):int {
			if (pYear < 0) throw new RangeError("year", "Non-negative number required.");
			
			if (pYear <= 99) {
				var year2:int = _twoDigitYearMax % 100;
				var d:int = pYear - year2;
				pYear = _twoDigitYearMax + d + (d <= 0 ? 0 : -100); 
			}
			
			_checkYE(pYear, [-1]);
			return pYear;
		}
		
	}
}
